/*
 * Code for blog.techhead.biz
 * Distributed under BSD-style license
 */

package biz.techhead.funcy;

import biz.techhead.funcy.Iterables.FuncyArrayList;
import biz.techhead.funcy.Iterables.FuncyList;
import biz.techhead.funcy.Tuples.T2;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Maps is a utility class for building and working with Maps.
 *
 * <code><pre>&nbsp;{@literal
 * import static biz.techhead.funcy.Maps.*;
 * import static biz.techhead.funcy.SugarFunc.*;
 *
 * // m1 and m2 are equivalent
 * Map<Integer,String> m1 = Map(1, "one", 2, "two", 3, "three");
 * Map<Integer,String> m2 = Map(1, "one").and(2, "two").and(3, "three");
 * 
 * // to make an immutable Map
 * final Map<Integer,String> m3 = Map(1, "one").immutable();
 * // or
 * final Map<Integer,String> m4 = immutable( Map(1, "one") );
 * 
 * // can query for a subset using a Predicate of type T2 (Tuple-2)
 * Map<Integer,String> odd = subset(m1, new Predicate<T2<Integer,?>>() {
 *                              void oddOnly() { out = ((in._1 % 2) != 0); } });
 *
 * // or a Predicate of type Map.Entry
 * Map<Integer,String> odd2 = subset(m1, new Predicate<Map.Entry<Integer,?>>() {
 *                        void oddOnly() { out = ((in.getKey() % 2) != 0); } });
 * }</pre></code>
 *
 * @see Iterables
 * @see SugarFunc
 * 
 * @author Jonathan Hawkes <jhawkes at techhead.biz>
 */
public class Maps {

    // prevent instantiation
    private Maps() {}

    /**
     * Constructs a new empty {@link Maps.BuildableMap}.  This Map will allow
     * null keys and values and will iterate over entries in the order that they
     * were added.
     */
    public static <K,V> BuildableMap<K,V> Map() {
        return new BuildableMapImpl<K,V>();
    }
    
    /**
     * Constructs a new {@link Maps.BuildableMap} (as with {@code Map()})
     * and adds 1 entry to it.
     */
    public static <K,V> BuildableMap<K,V> Map(K key, V value) {
        BuildableMap<K,V> map = Map();
        return map.and(key, value);
    }
    
    /**
     * Constructs a new {@link Maps.BuildableMap} (as with {@code Map()})
     * and adds 2 entries to it.
     */
    public static <K,V> BuildableMap<K,V> Map(K k0, V v0, K k1, V v1) {
        BuildableMap<K,V> map = Map();
        return map.and(k0, v0, k1, v1);
    }
    
    /**
     * Constructs a new {@link Maps.BuildableMap} (as with {@code Map()})
     * and adds 3 entries to it.
     */
    public static <K,V> BuildableMap<K,V> Map(K k0, V v0, K k1, V v1,
            K k2, V v2) {
        BuildableMap<K,V> map = Map();
        return map.and(k0, v0, k1, v1, k2, v2);
    }
    
    /**
     * Constructs a new {@link Maps.BuildableMap} (as with {@code Map()})
     * and adds 4 entries to it.
     */
    public static <K,V> BuildableMap<K,V> Map(K k0, V v0, K k1, V v1,
            K k2, V v2, K k3, V v3) {
        BuildableMap<K,V> map = Map();
        return map.and(k0, v0, k1, v1, k2, v2, k3, v3);
    }
    
    /**
     * Constructs a new BuildableMap (as with {@code Map()})
     * and adds 5 entries to it.
     */
    public static <K,V> BuildableMap<K,V> Map(K k0, V v0, K k1, V v1,
            K k2, V v2, K k3, V v3, K k4, V v4) {
        BuildableMap<K,V> map = Map();
        return map.and(k0, v0, k1, v1, k2, v2, k3, v3, k4, v4);
    }

    /**
     * Creates a copy of the given Map as a {@link Maps.BuildableMap},
     * iteration order preserved.
     */
    public static <K,V>
            BuildableMap<K,V> copy(Map<? extends K,? extends V> map){
        return new BuildableMapImpl<K,V>(map);
    }

    /**
     * Returns a Map with the same keys, values and the same ordering of the
     * entries -- only the returned Map cannot be changed.  If the original
     * source Map changes, this immutable Map will still not change.
     *
     * <p>The returned Map will be a Google Collections {@code ImmutableMap} if
     * Google Collections API is available during runtime.
     */
    @SuppressWarnings("unchecked")
    public static <K,V>
            Map<K,V> immutable(Map<? extends K,? extends V> map) {
        return ( Map<K,V> ) toImmutable.call(map);
    }

    /**
     * This method is the default implementation of immutability for Maps made
     * immutable by this class.  This will not be called unless Google
     * Collections is unavailable or the Map to be made immutable contains a
     * null key or value.
     *
     * This implementation simply copies the Map into a new LinkedHashMap
     * and then returns an unmodifiable view of the copy.
     */
    private static <K,V>
            Map<K,V> makeImmutable(Map<? extends K,? extends V> map) {
        return Collections.unmodifiableMap( new LinkedHashMap<K,V>(map) );
    }

    /**
     * Applies the given function to every {@code Map.Entry} of the given Map
     * and adds each result to the supplied Collection.
     * @return the supplied Collection
     */
    public static <K,V,O,C extends Collection<? super O>,E extends Throwable>
            C map(Map<? extends K,? extends V> map,
                  FuncE<O,? super MapEntry<K,V>,E> f, C out) throws E {
        for (Map.Entry<? extends K,? extends V> entry : map.entrySet()) {
            out.add( f.call( new MapEntry<K,V>(entry) ) );
        }
        return out;
    }

    /**
     * Applies the given function to every {@code Map.Entry} of the given Map
     * and returns a List of the results.
     */
    public static <K,V,O,E extends Throwable>
            FuncyList<O> map(Map<? extends K,? extends V> map,
                             FuncE<O,? super MapEntry<K,V>,E> f) throws E {
        return map(map, f, new FuncyArrayList<O>(map.size()));
    }

    /**
     * Takes a function that serves as a predicate and iterates over
     * every {@code Map.Entry} testing it.  Entries selected by the predicate
     * are added to the supplied Map.
     * @return the supplied Map
     */
    public static <K,V,K2 extends K,V2 extends V,
                M extends Map<? super K2,? super V2>,
                E extends Throwable>
            M subset(Map<K2,V2> map,
                     FuncE<Boolean,? super MapEntry<K,V>,E> f,
                     M out) throws E {
        for (Map.Entry<K2,V2> entry : map.entrySet()) {
            if ( f.call( new MapEntry<K,V>(entry) ) ) {
                out.put(entry.getKey(), entry.getValue());
            }
        }
        return out;
    }

    /**
     * Takes a function that serves as a predicate and iterates over
     * every {@code Map.Entry} testing it.  Entries selected by the predicate
     * are added to the returned Map, iteration order preserved.
     */
    public static <K,V,K2 extends K,V2 extends V,E extends Throwable>
            BuildableMap<K2,V2> subset(Map<K2,V2> map,
                                       FuncE<Boolean,? super MapEntry<K,V>,E> f)
            throws E {
        return subset(map, f, new BuildableMapImpl<K2,V2>());
    }

    /**
     * Visits each {@code Map.Entry} with the provided function.
     */
    public static <K,V,E extends Throwable>
            void each(Map<? extends K,? extends V> map,
                      FuncE<?,? super MapEntry<K,V>,E> f) throws E {
        for (Map.Entry<? extends K,? extends V> entry : map.entrySet()) {
            f.call( new MapEntry<K,V>(entry) );
        }
    }

    /**
     * Returns true if the result of testing each {@code Map.Entry} against the
     * given predicate function is also true.  Returns false otherwise.
     */
    public static <K,V,E extends Throwable>
            boolean every(Map<? extends K,? extends V> map,
                          FuncE<Boolean,? super MapEntry<K,V>,E> f) throws E {
        for (Map.Entry<? extends K,? extends V> entry : map.entrySet()) {
            if ( !f.call( new MapEntry<K,V>(entry) ) ) return false;
        }
        return true;
    }

    /**
     * Returns true if the result of testing any {@code Map.Entry} against the
     * given predicate function is also true.  Returns false otherwise.
     */
    public static <K,V,E extends Throwable>
            boolean any(Map<? extends K,? extends V> map,
                        FuncE<Boolean,? super MapEntry<K,V>,E> f) throws E {
        for (Map.Entry<? extends K,? extends V> entry : map.entrySet()) {
            if ( f.call( new MapEntry<K,V>(entry) ) ) return true;
        }
        return false;
    }

    /**
     * A {@code BuildableMap} is built incrementally by using Java's dot
     * syntax to extend the creation expression.  It simply adds a few
     * {@code and} methods to the {@code Map} interface that function in the
     * same manner as {@code put} and {@code putAll}, however the return value
     * is the modified {@code BuildableMap}.
     *
     * <code><pre>&nbsp;{@literal
     * import static biz.techhead.funcy.Maps.Map;
     * Map<Integer,String> nums = Map(1, "one")
     *                           .and(2, "two")
     *                           .and(3, "three")
     *                           .immutable();
     * }</pre></code>
     */
    public static interface BuildableMap<K,V> extends Map<K,V> {

        /**
         * Puts 1 entry into this Map and then returns it.
         */
        BuildableMap<K,V> and(K key, V value);

        /**
         * Puts 2 entries into this Map and then returns it.
         */
        BuildableMap<K,V> and(K k0, V v0, K k1, V v1);

        /**
         * Puts 3 entries into this Map and then returns it.
         */
        BuildableMap<K,V> and(K k0, V v0, K k1, V v1, K k2, V v2);

        /**
         * Puts 4 entries into this Map and then returns it.
         */
        BuildableMap<K,V> and(K k0, V v0, K k1, V v1, K k2, V v2, K k3, V v3);

        /**
         * Puts 5 entries into this Map and then returns it.
         */
        BuildableMap<K,V> and(K k0, V v0, K k1, V v1, K k2, V v2, K k3, V v3,
                K k4, V v4);

        /**
         * Calls {@code this.putAll(map)} and then returns {@code this} Map.
         */
        BuildableMap<K,V> andAll(Map<? extends K, ? extends V> map);

        /**
         * Same as {@link Maps#immutable(Map) Maps.immutable(this)}
         */
        Map<K,V> immutable();
    }
    
    private static class BuildableMapImpl<K,V> extends LinkedHashMap<K,V>
            implements BuildableMap<K,V> {
                
        BuildableMapImpl() {}
        BuildableMapImpl(Map<? extends K,? extends V> map) {
            super(map);
        }

        public BuildableMap<K, V> and(K key, V value) {
            put(key, value);
            return this;
        }

        public BuildableMap<K, V> and(K k0, V v0, K k1, V v1) {
            put(k0, v0);
            put(k1, v1);
            return this;
        }

        public BuildableMap<K, V> and(K k0, V v0, K k1, V v1, K k2, V v2) {
            put(k0, v0);
            put(k1, v1);
            put(k2, v2);
            return this;
        }

        public BuildableMap<K, V> and(K k0, V v0, K k1, V v1, K k2, V v2,
                K k3, V v3) {
            put(k0, v0);
            put(k1, v1);
            put(k2, v2);
            put(k3, v3);
            return this;
        }

        public BuildableMap<K, V> and(K k0, V v0, K k1, V v1, K k2, V v2,
                K k3, V v3, K k4, V v4) {
            put(k0, v0);
            put(k1, v1);
            put(k2, v2);
            put(k3, v3);
            put(k4, v4);
            return this;
        }

        public BuildableMap<K, V> andAll(Map<? extends K, ? extends V> map) {
            putAll(map);
            return this;
        }

        public Map<K,V> immutable() {
            return Maps.immutable(this);
        }
    }

    /**
     * Implements the {@code Map.Entry} interface and extends {@code Tuples.T2}.
     * This is the concrete Entry class that is passed to all functions
     * provided in the {@code map, subset, each, every}, and {@code any} static
     * methods.
     */
    public static class MapEntry<K,V> extends Tuples.T2<K,V>
            implements Map.Entry<K,V> {

        MapEntry(Map.Entry<? extends K,? extends V> e) {
            super(e.getKey(), e.getValue());
        }

        /**
         * {@inheritDoc}
         */
        public K getKey() { return _1; }

        /**
         * {@inheritDoc}
         */
        public V getValue() { return _2; }

        /**
         * Unsupported Operation
         *
         * @throws UnsupportedOperationException
         */
        public V setValue(V value) {
            throw new UnsupportedOperationException();
        }
    }

    /**
     * This function is used to transform a given Map into an immutable one.
     * If the Google Collections API is available, it will use that.
     */
    private static final Func<Map,Map> toImmutable;

    /*
     * This ugly little bit of code attempts to use the Google Collections API
     * without directly depending on it.
     *
     * If Google Collections is found in the classpath, then immutable Maps
     * will be constructed using com.google.common.collect.ImmutableMap.copyOf
     */
    static {
        Func<Map,Map> temp;
        try {
            final Method m =
                    Class.forName("com.google.common.collect.ImmutableMap")
                        .getMethod("copyOf");
            temp = new Func<Map,Map>() {
                @SuppressWarnings("unchecked")
                public Map call(Map in) {
                    try {
                        return (Map) m.invoke(null, in);
                    } catch (IllegalAccessException iae) {
                        // The method IS accessible
                        throw new AssertionError();
                    } catch (InvocationTargetException ite) {
                        Throwable cause = ite.getCause();
                        if (cause instanceof NullPointerException) {
                            // We had a null key or value.
                            // Just do it the old-fashioned way.
                            return makeImmutable(in);
                        }
                        throw new AssertionError("Google Collections API" +
                                " changed - unexpected Exception from" +
                                " ImmutableMap.copyOf");
                    }
                }
            };
        } catch (Exception ex) {
               //ClassNotFoundException
               //NoSuchMethodException
            temp = new Func<Map,Map>() {
                @SuppressWarnings("unchecked")
                public Map call(Map in) {
                    return makeImmutable(in);
                }  
            };
        }
        toImmutable = temp;
    }
}
